package hibatis.support.generator;

import hibatis.annotation.*;
import hibatis.ext.InterceptorContext;
import hibatis.support.Reflection;
import hibatis.support.StringUtils;
import hibatis.support.parse.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by huangdachao on 2018/6/28 16:12.
 */
public class FromAndWhereGenerator {
    private String[] argNames;
    private List<FilterMeta> filterMeta;
    private List<JoinMeta> joinMeta;

    private EntityMeta em;
    private ViewMeta vm;
    private PageAndSort pas = new PageAndSort();
    private Map<Table, String> tblAlias = new HashMap<>();
    private Map<String, Object> additionalParams = new HashMap<>();

    /**
     * 用于HasOne或HasMany字段的二次查询
     * @param field
     */
    public FromAndWhereGenerator(Field field) {
        em = EntityMeta.get(getFieldEntityType(field));
        HasOne ho = field.getAnnotation(HasOne.class);
        HasMany hm = field.getAnnotation(HasMany.class);
        Filter[] filters = null;

        if (ho != null) {
            filters = ho.filter();
            vm = ViewMeta.parse(em, ho.resultView());
            if (!ho.orderBy().isEmpty()) {
                pas.setSort(PageAndSort.SortMeta.parseSort(em, hm.orderBy()));
                pas.setLimit(1L);
            } else if (ho.forceOne()) {
                pas.setLimit(1L);
            }
        } else if (hm != null) {
            filters = hm.filter();
            vm = ViewMeta.parse(em, hm.resultView());
            if (!hm.orderBy().isEmpty()) {
                pas.setSort(PageAndSort.SortMeta.parseSort(em, hm.orderBy()));
            }
            if (hm.limit() > 0) {
                pas.setLimit((long) hm.limit());
            }
        }
        if (filters == null || filters.length == 0) {
            throw new RuntimeException("请指定关联条件：" + Reflection.formatField(field));
        }

        // resolve FilterMeta
        List<FilterMeta> filterMetas = new ArrayList<>();
        for (Filter f : filters) {
            if (!f.ignore()) {
                TableColumn tc = new TableColumn(f.table(), f.tblAlias(), f.column());
                if (tc.column.isEmpty()) {
                    throw new RuntimeException("必须指定@Filter的column属性：" + Reflection.formatField(field));
                }
                if (tc.table.isEmpty()) {
                    tc.table = em.getTable().table;
                }
                if (f.placeholder().isEmpty()) {
                    throw new RuntimeException("@HasOne或@HasMany字段必须指定@Filter的placeholder属性：" + Reflection.formatField(field));
                }

                FilterMeta fm = new FilterMeta();
                fm.setTableColumn(tc);
                fm.setArgIndex(0);
                fm.setType(f.type());
                fm.setOrder(f.order());
                fm.setPlaceholder(f.placeholder());
                fm.getReferTables().addAll(Table.parse(fm.getPlaceholder(), em));
                filterMetas.add(fm);
            }
        }
        filterMeta = filterMetas.stream().sorted(Comparator.comparingInt(FilterMeta::getOrder))
            .collect(Collectors.toList());

        // resolve JoinMeta
        Set<Table> tables = new HashSet<>(vm.getTables());
        filterMeta.forEach(fm -> tables.addAll(fm.getReferTables()));
        pas.getSort().forEach(s -> tables.add(s.getTableColumn().toTable()));
        joinMeta = resolveJoinMeta(tables, em, new Object[0], new Annotation[0][]);
        tblAlias.put(em.getTable(), "t0");
        for (int i = 0; i < joinMeta.size(); i++) {
            tblAlias.put(joinMeta.get(i).getTable(), "t" + (i + 1));
        }
    }

    public FromAndWhereGenerator(EntityMeta em, Method method, Object[] args, String[] argNames, InterceptorContext context) {
        this(em, method, args, argNames, context, null);
    }

    public FromAndWhereGenerator(EntityMeta em, Method method, Object[] args, String[] argNames, InterceptorContext context, Set<Table> additionalTables) {
        this.em = em;
        this.argNames = argNames;

        // initialize ViewMeta and PageAndSort and default filter
        Annotation[] annotations = method.getAnnotations();
        Filter[] filters = new Filter[0];
        for (int i = 0; i < annotations.length; i++) {
            Class<?> type = annotations[i].annotationType();
            if (type == ExecuteSelect.class) {
                ExecuteSelect es = (ExecuteSelect) annotations[i];
                vm = ViewMeta.parse(em, es.view());
                if (es.selectOne()) {
                    pas.setLimit(1L);
                    pas.setSelectOne(true);
                }
                if (!es.orderBy().isEmpty()) {
                    pas.setSort(PageAndSort.SortMeta.parseSort(em, es.orderBy()));
                }
                filters = es.filter();
            } else if (type == ExecuteCount.class) {
                ExecuteCount ec = (ExecuteCount) annotations[i];
                pas.setSelectCount(true);
                filters = ec.filter();
            } else if (type == ExecuteDelete.class) {
                ExecuteDelete ed = (ExecuteDelete) annotations[i];
                filters = ed.filter();
            }
        }

        // process FilterMeta
        Map<TableColumn, List<FilterMeta>> filterMap = FilterMeta.parseArgs(em, method, args, pas);
        FilterMeta.mergeFromFilters(filters, em, filterMap);
        FilterMeta.margeFromInterceptorContext(context, additionalParams, filterMap);
        filterMeta = filterMap.values().stream().flatMap(Collection::stream).collect(Collectors.toList());

        Set<Table> tables = new HashSet<>();
        if (vm != null) {
            tables.addAll(vm.getTables());
        }
        if (additionalTables != null) {
            tables.addAll(additionalTables);
        }
        filterMap.values().stream().flatMap(Collection::stream).forEach(fm -> tables.addAll(fm.getReferTables()));
        pas.getSort().forEach(s -> tables.add(s.getTableColumn().toTable()));

        // parse JoinMeta and generate tblAlias
        joinMeta = resolveJoinMeta(tables, em, args, method.getParameterAnnotations());
        tblAlias.put(em.getTable(), "t0");
        for (int i = 0; i < joinMeta.size(); i++) {
            tblAlias.put(joinMeta.get(i).getTable(), "t" + (i + 1));
        }
    }

    public String generate() {
        return new StringBuilder(" from ").append(em.getTable().table).append(" t0 ")
            .append(generateJoinClause())
            .append(generateWhereClause())
            .append(" ").toString();
    }

    public String generateJoinClause() {
        if (joinMeta.size() == 0) {
            return " ";
        }

        StringBuilder sql = new StringBuilder();
        Set<Table> available = new HashSet<>();
        Set<JoinMeta> asked = new HashSet<>(joinMeta);
        available.add(em.getTable());
        boolean updated = true;

        while (updated && asked.size() > 0) {
            updated = false;
            Iterator<JoinMeta> iterator = asked.iterator();
            while (iterator.hasNext()) {
                JoinMeta jm = iterator.next();
                Set<Table> tables = jm.getReferTables();
                if (available.containsAll(tables)) {
                    available.add(jm.getTable());
                    iterator.remove();

                    String alias = tblAlias.get(jm.getTable());
                    if (alias == null) {
                        throw new RuntimeException("未解析的数据表：" + jm.getTable());
                    }
                    sql.append(jm.getJoinType()).append(" join ").append(jm.getTable().table).append(" ").append(alias).append(" on ");

                    boolean first = true;
                    for (JoinConstraint c : jm.getConstraints()) {
                        if (first) {
                            first = false;
                        } else {
                            sql.append(" and ");
                        }
                        sql.append(alias).append(".").append(c.getColumn()).append("=")
                                .append(tblAlias.get(c.getReferTable())).append(".").append(c.getReferColumn());
                    }

                    for (Filter f : jm.getFilters()) {
                        if (StringUtils.isEmpty(f.placeholder()) || StringUtils.isEmpty(f.column())) {
                            throw new RuntimeException("@Join注解的关联条件必须指定placeholder值和column值");
                        }

                        sql.append(" and ").append(tblAlias.get(new Table("".equals(f.table()) ? jm.getTable().table : f.table(), f.tblAlias())))
                                .append(".")
                                .append(f.column())
                                .append(f.type().toSql(f.placeholder(), null));
                    }
                    sql.append(" ");

                    updated = true;
                    if (asked.size() == 0) {
                        break;
                    }
                }
            }
        }

        if (asked.size() > 0) {
            throw new RuntimeException("关联表未找到，" + asked.stream()
                    .map(t -> t.getTable().table + (t.getTable().alias.isEmpty() ? "" : " " + t.getTable().alias))
                    .collect(Collectors.joining(",")));
        }
        return sql.toString();
    }

    public String generateWhereClause() {
        StringBuilder sql = new StringBuilder();
        if (filterMeta.size() > 0) {
            sql.append("where ");
            boolean first = true;
            for (int i = 0; i < filterMeta.size(); i++) {
                FilterMeta fm = filterMeta.get(i);
                if (first) {
                    first = false;
                } else {
                    sql.append(" and ");
                }

                if (StringUtils.isEmpty(fm.getPlaceholder())) {
                    String field = fm.getField() == null ? "" : "." + fm.getField();
                    if (argNames.length > 1) {
                        fm.setPlaceholder(argNames[fm.getArgIndex()] + field);
                    } else {
                        fm.setPlaceholder(StringUtils.isEmpty(field) ? argNames[0] : field.substring(1));
                    }
                } else {
                    fm.setPlaceholder(StringUtils.normalizeTableAndFieldExpression(fm.getPlaceholder(), em, tblAlias));
                }

                sql.append(tblAlias.get(fm.getTableColumn().toTable())).append(".")
                    .append(fm.getTableColumn().column)
                    .append(fm.getType().toSql(fm.getPlaceholder(), fm.getValue()));
            }
        }
        return sql.toString();
    }

    public static List<JoinMeta> resolveJoinMeta(Set<Table> tables, EntityMeta em, Object[] args, Annotation[][] annotations) {
        List<JoinMeta> joinMeta = new ArrayList<>(em.getJoinMap().values());
        Set<Table> asked = new HashSet<>(tables);
        Set<Table> available = new HashSet<>();
        asked.remove(em.getTable());
        available.add(em.getTable());

        for (int i = 0; i < args.length; i++) {
            FilterParam fp = Reflection.findAnnotation(annotations[i], FilterParam.class);
            if (fp != null && fp.join().length > 0) {
                Arrays.stream(fp.join()).forEach(join -> joinMeta.add(new JoinMeta(join, em)));
            }

            if (args[i] != null) {
                Class<?> argType = args[i].getClass();
                Query query = argType.getAnnotation(Query.class);
                if (query != null && query.join().length > 0) {
                    Arrays.stream(query.join()).forEach(join -> joinMeta.add(new JoinMeta(join, em)));
                }
            }
        }

        List<JoinMeta> selected = new ArrayList<>();
        if (asked.size() > 0) {
            boolean updated = true;
            while (updated && asked.size() > 0) {
                updated = false;
                Iterator<JoinMeta> iterator = joinMeta.iterator();
                while (iterator.hasNext()) {
                    JoinMeta jm = iterator.next();
                    Table t = jm.getTable();
                    if (asked.contains(t)) {
                        selected.add(jm);
                        available.add(t);
                        asked.remove(t);
                        iterator.remove();
                        jm.getReferTables().forEach(r -> {
                            if (!StringUtils.isEmpty(r.table) && !available.contains(r)) {
                                asked.add(r);
                            }
                        });
                        updated = true;
                        if (asked.size() == 0) {
                            break;
                        }
                    }
                }
            }
        }

        if (asked.size() > 0) {
            throw new RuntimeException("关联表未找到，" + asked.stream()
                    .map(t -> t.table + (t.alias.isEmpty() ? "" : " " + t.alias))
                    .collect(Collectors.joining(",")));
        }
        return selected;
    }

    private static Class<?> getFieldEntityType(Field field) {
        Class<?> fieldType = field.getType();
        if (Collection.class.isAssignableFrom(fieldType)) {
            fieldType = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
        } else if (fieldType.isArray()) {
            fieldType = fieldType.getComponentType();
        }
        return fieldType;
    }

    public String[] getArgNames() {
        return argNames;
    }

    public void setArgNames(String[] argNames) {
        this.argNames = argNames;
    }

    public List<FilterMeta> getFilterMeta() {
        return filterMeta;
    }

    public void setFilterMeta(List<FilterMeta> filterMeta) {
        this.filterMeta = filterMeta;
    }

    public List<JoinMeta> getJoinMeta() {
        return joinMeta;
    }

    public void setJoinMeta(List<JoinMeta> joinMeta) {
        this.joinMeta = joinMeta;
    }

    public EntityMeta getEntityMeta() {
        return em;
    }

    public ViewMeta getViewMeta() {
        return vm;
    }

    public PageAndSort getPageAndSort() {
        return pas;
    }

    public Map<Table, String> getTableAliasMap() {
        return tblAlias;
    }

    public Map<String, Object> getAdditionalParams() {
        return additionalParams;
    }
}
